/*
 * Copyright the original author or authors.
 * 
 * Licensed under the MOZILLA PUBLIC LICENSE, Version 1.1 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.mozilla.org/MPL/MPL-1.1.html
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.customactions.flexunit.listener 
{
	import flex.lang.reflect.Klass;
	import flex.lang.reflect.metadata.MetaDataAnnotation;
	import flex.lang.reflect.metadata.MetaDataArgument;

	import flexunit.framework.TestSuite;

	import net.customactions.flexunit.patterns.AbstractPattern;
	import net.customactions.flexunit.patterns.AssertContainedPattern;
	import net.customactions.flexunit.patterns.AssertEqualsPattern;
	import net.customactions.flexunit.patterns.AssertEventOcurredPattern;
	import net.customactions.flexunit.patterns.AssertMatchPattern;
	import net.customactions.flexunit.patterns.AssertNoMatchPattern;
	import net.customactions.flexunit.patterns.AssertNotContainedPattern;
	import net.customactions.flexunit.patterns.AssertNotNullPattern;
	import net.customactions.flexunit.patterns.AssertNotUndefinedPattern;
	import net.customactions.flexunit.patterns.AssertNullPattern;
	import net.customactions.flexunit.patterns.AssertThatPattern;
	import net.customactions.flexunit.patterns.AssertUndefinedPattern;
	import net.customactions.flexunit.patterns.FailAsyncCallPattern;
	import net.customactions.flexunit.patterns.GreaterThanPattern;
	import net.customactions.flexunit.patterns.PatternResult;
	import net.customactions.flexunit.util.StringHelper;
	import net.customactions.flexunit.view.XMLReportView;

	import org.flexunit.Assert;
	import org.flexunit.runner.IDescription;
	import org.flexunit.runner.Result;
	import org.flexunit.runner.notification.Failure;
	import org.flexunit.runner.notification.IRunListener;

	import flash.utils.Dictionary;
	import flash.utils.describeType;
	import flash.utils.getQualifiedClassName;
	import flash.utils.getTimer;

	/**
	 * The XMLReportListener listener build correct XML data using UnitTest 
	 * results.
	 * 
	 * @includeExample XMLReportListenerExample.as -noswf
	 * 
	 * @author Romain Ecarnot
	 * 
	 * @langversion 3.0
	 * @playerversion Flash 10
	 * @playerversion AIR 1.5
	 * @productversion FDT 3.5
	 */
	public class XMLReportListener implements IRunListener, SuitableListener
	{
		//--------------------------------------------------------------------
		// Constants
		//--------------------------------------------------------------------
		
		/**
		 * Default TestSuite name if none is found during FlexUnit testing.
		 * 
		 * @default DefaultSuite
		 * 
		 * @langversion 3.0
		 * @playerversion Flash 10
		 * @playerversion AIR 1.5
		 * @productversion FDT 3.5
		 */
		public static const DEFAULT_TESTSUITE_NAME : String = "DefaultSuite";

		
		//--------------------------------------------------------------------
		// Private properties
		//--------------------------------------------------------------------
		
		/**
		 * @private
		 * Patterns list for failure message parsing
		 */
		private static const patterns : Array = [
			new AssertNotContainedPattern(),
            new AssertNoMatchPattern(),
            new AssertMatchPattern(),
            new AssertContainedPattern(),
            new AssertEventOcurredPattern(),
            new AssertEqualsPattern(),
            new AssertNotNullPattern(),
            new AssertNotUndefinedPattern(),
            new AssertNullPattern(),
            new AssertUndefinedPattern(),
            new FailAsyncCallPattern(),
            new AssertThatPattern(),
			new GreaterThanPattern()
		];

		/**
		 * @private 
		 * Stores last failed test to avoid current FlexUnit version bug.
		 */
		private var _lastFailedTest : IDescription;

		/**
		 * @private
		 * Available TestSuite in current FlexUnit Testing session.
		 */
		private var _suites : Dictionary;

		/**
		 * @private 
		 * Tests / Suite relational tree
		 */
		private var _suiteTestMap : Dictionary;

		/**
		 * @private
		 * Number of assertions for a test
		 */
		private var _assertCount : uint;

		/**
		 * @private
		 * Current tested suite name
		 */
		private var _currentTestName : String;

		/**
		 * @private
		 * Stores TestSuite nodes
		 */		
		private var _suiteNodes : Dictionary;

		/**
		 * @private
		 * Stores TestCase node
		 */		private var _caseNodes : Dictionary;

		/**
		 * @private
		 * Result XML
		 */
		private var _xml : XML;

		/**
		 * @private
		 *
		 */
		private var _timer : int; 

		/**
		 * @private
		 * Stores XMLReport views
		 */
		private var _views : Vector.<XMLReportView>;

		
		//--------------------------------------------------------------------
		// Public API
		//--------------------------------------------------------------------
		
		/**
		 * Creates instance.
		 * 
		 * @langversion 3.0
		 * @playerversion Flash 10
		 * @playerversion AIR 1.5
		 * @productversion FDT 3.5
		 */
		public function XMLReportListener() 
		{
			_suites = new Dictionary();
			_suiteTestMap = new Dictionary();
			_suiteNodes = new Dictionary();			_caseNodes = new Dictionary();
			_views = new Vector.<XMLReportView>();
		}

		/**
		 * Adds TestSuite.
		 * 
		 * <p>This operation is optional but allow to retreive TestSuite 
		 * information in XML report.<br />
		 * In not use, all TestCase will be registered in default FlexUnit test 
		 * xml node.</p>
		 * 
		 * @langversion 3.0
		 * @playerversion Flash 10
		 * @playerversion AIR 1.5
		 * @productversion FDT 3.5
		 */
		public function registerSuite(suites : Array) : void
		{
			for each (var suite : * in suites) 
			{
				if(!_inherit(suite, TestSuite))
				{
					var klass : Klass = new Klass(suite);
					if(!klass.hasMetaData("Suite") && !klass.hasMetaData("RunWith"))
					{
						return;	
					}
				}
				
				_suites[getQualifiedClassName(suite)] = true;
			}
		}

		/**
		 * Registers passed-in view as XMLReport view.
		 * 
		 * @param view	XMLReport view to register
		 * 
		 * @langversion 3.0
		 * @playerversion Flash 10
		 * @playerversion AIR 1.5
		 * @productversion FDT 3.5
		 */
		public function registerView(view : XMLReportView) : void
		{
			if(_views.indexOf(view) > 0) return;
			
			_views[_views.length] = view;
		}

		/**
		 * @private
		 * Triggered when testing starts.
		 */
		final public function testRunStarted(description : IDescription) : void
		{
			_assertCount = 0;
			_timer = getTimer();
			
			_xml = <flexunit testCount={description.testCount} successful="true"/>;
			
			_resolveParents(description, new Dictionary());
		}

		/**
		 * @private
		 * Triggered when a test starts
		 */
		public function testStarted(description : IDescription) : void
		{
			_currentTestName = description.displayName;
		}

		/**
		 * @private
		 */
		public function testAssumptionFailure(failure : Failure) : void
		{
		}

		/**
		 * @private
		 * Triggered when a failure test occurs.
		 */
		final public function testFailure(failure : Failure) : void
		{
			_lastFailedTest = failure.description;
			
			buildResult(failure.description, false, false, failure);
		}

		/**
		 * @private
		 * Triggered when an ignored test is found.
		 */
		final public function testIgnored(description : IDescription) : void
		{
			buildResult(description, true, true);
		}

		/**
		 * @private
		 * Triggered when a test is finished.
		 */
		final public function testFinished(description : IDescription) : void
		{
			if(!_lastFailedTest || description.displayName != _lastFailedTest.displayName)
			{
				buildResult(description, true);
			}
		}

		/**
		 * @private
		 * Triggered when all Tetss are completed. 
		 */
		public function testRunFinished(result : Result) : void
		{
			_xml.@assertions = _assertCount;
			_xml.@time = getTimer() - _timer;
			
			if(_views.length > 0)
			{
				for each (var view : XMLReportView in _views) view.process(_xml); 
			}
		}
		
		/**
		 * Returns String representation of instance.
		 *
		 * @return String representation of instance.
		 * 
		 * @langversion 3.0
		 * @playerversion Flash 10
		 * @playerversion AIR 1.5
		 * @productversion FDT 3.5
		 */
		public function toString() : String
		{
			return getQualifiedClassName(this) + "[" + _views.length + " views]";
		}
		

		//--------------------------------------------------------------------
		// Protected methods
		//--------------------------------------------------------------------
		
		/**
		 * Builds XML description of passed-in description result.
		 * 
		 * @langversion 3.0
		 * @playerversion Flash 10
		 * @playerversion AIR 1.5
		 * @productversion FDT 3.5
		 */
		protected function buildResult(description : IDescription, success : Boolean, ignored : Boolean = false, failure : Failure = null ) : void
		{
			if(!success) _xml.@successful = false;
			
			_assertCount += Assert.assertionsMade;
			var assertCount : uint = Assert.assertionsMade;
			Assert.resetAssertionsFields();
			
			var suiteName : String = _suites[_suiteTestMap[_currentTestName]] != null ? _suiteTestMap[_currentTestName] : DEFAULT_TESTSUITE_NAME;
			if(_suiteNodes[suiteName] == null)
			{
				_suiteNodes[suiteName] = <suite id={suiteName} successful="true" />;
				_xml.appendChild(_suiteNodes[suiteName]);
			}
			var suiteNode : XML = _suiteNodes[suiteName];
			
			var caseName : String = StringHelper.getCaseName(_currentTestName);
			if(_caseNodes[caseName] == null)
			{
				_caseNodes[caseName] = <testcase id={caseName} name={StringHelper.getClassName(caseName)} successful="true" assertions="0" />;
				suiteNode.appendChild(_caseNodes[caseName]);
			}
			var caseNode : XML = _caseNodes[caseName];
			
			caseNode.appendChild(buildTestNode(description, success, ignored, assertCount, failure));
			
			caseNode.@assertions = parseInt(caseNode.@assertions) + assertCount;
			
			if(!success) 
			{
				caseNode.@successful = false;
				caseNode.parent().@successful = false;
			}
		}

		/**
		 * Builds and returns Test XML node.
		 * 
		 * @langversion 3.0
		 * @playerversion Flash 10
		 * @playerversion AIR 1.5
		 * @productversion FDT 3.5
		 */
		protected function buildTestNode(description : IDescription, success : Boolean, ignored : Boolean, asserts : uint = 0, failure : Failure = null) : XML
		{
			var node : XML = <test methodName={StringHelper.getMethodName(description.displayName)} successful={success} ignored={ignored} assertions={asserts}/>;
				
			if(failure != null)
			{
				var result : PatternResult = new PatternResult();
				for ( var i : int = 0 ;i < patterns.length;i++ )
				{
					var pattern : AbstractPattern = AbstractPattern(patterns[ i ]);
 					
					if( pattern.match(failure.message) )
					{
						result = pattern.apply();
						
						break;
					}
				}
				
				node.appendChild(<failure actual={result.actualResult} expected={result.expectedResult}>
						<message>{failure.message}</message>
						<stack>{StringHelper.formatStackTrace(failure.stackTrace)}</stack>
					</failure>);
			}
			
			for each (var annotation : MetaDataAnnotation in description.getAllMetadata()) 
			{
				for each (var argument : MetaDataArgument in annotation.arguments) 
				{
					node.appendChild(<metadata key={argument.key} value={argument.value}/>);
				}
			}
			
			return node;
		}

		
		//--------------------------------------------------------------------
		// Private methods
		//--------------------------------------------------------------------
		
		/**
		 * @private
		 * Checks if passed clazz inherit from parent class.
		 */
		private function _inherit( clazz : Class, parent : Class) : Boolean 
		{
			var xml : XML = describeType(clazz);
			var parentName : String = getQualifiedClassName(parent);
			return 	(xml.factory.extendsClass.@type).contains(parentName) || (xml.factory.implementsInterface.@type).contains(parentName);
		}

		/**
		 * @private
		 * Resolves Test ans Suite tree
		 */
		private function _resolveParents(description : IDescription, parents : Dictionary) : void 
		{
			for each (var child:IDescription in description.children) 
			{
				parents[child] = description;
 				
				if (child.isSuite) _resolveParents(child, parents);
				if (child.isTest) _resolveSuitename(child, parents);
			}
		}

		/**
		 * @private
		 * Creates suite name / Test name key/value pair
		 */
		private function _resolveSuitename(description : IDescription, parents : Dictionary) : void 
		{
			var suite : IDescription = description;
			while (parents[suite] != null && IDescription(parents[suite]).displayName.length > 0) suite = parents[suite];
			_suiteTestMap[description.displayName] = suite.displayName;
		}
	}
}